实现继承是 JS 唯一支持的继承方式,而这主要是通过原型链实现的,其基本思想就是通过原型继承多个引用类型的属性和方法。
重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链,以下代码展示一个基本的原型链:
// 定义 SuperType 类型,并分别定义一个属性和一个方法
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
// 定义 SubType 类型,并分别定义一个属性和一个方法
function SubType() {
this.subproperty = false;
}
SubType.prototype.getSubValue = function () {
return this.subproperty;
}
// 通过设置 prototype 使 SubType 继承自 SuperType
SubType.prototype = new SuperType();
let instance = new SubType();
// 在 SubType 实例上调用 SuperType 的方法
// 促使实例通过原型链找到指定方法
console.log(instance.getSuperValue()); // true
// 所有引用类型都继承自 Object
console.log(instance.toString()); // [object Object]
上述例子中实现继承的关键,是 SubType 没有使用默认原型,而是将其替换成了一个新的对象,即 SuperType 的实例。这样一来,SubType 的实例不仅能从 SuperType 的实例中继承属性和方法,而且还与 SuperType 的原型挂上了钩。于是 instance 通过内部的 [[Prototype]] 指向 SubType.prototype,而 SubType.prototype 又通过内部的 [[Prototype]] 指向 SuperType.prototype。
原型与实例的关系可以通过两种方式来确定。第一种方式是使用 instanceof 操作符,第二种方式是使用 isPrototypeOf()方法。
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
原型链的问题
原型链虽然是实现继承的强大工具,但它也有问题:
原型中包含的引用值会在所有实例间共享
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType(); console.log(instance2.colors); // "red,blue,green,black"
子类型无法在不 影响所有对象实例的情况下把参数传进父类的构造函数
为了解决以上两个问题,可以使用“盗用构造函数”的技术来实现。基本思路如下:
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 子类构造函数调用 appley() 或者 call() 方法,继承 SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
还可以通过该方法向父类构造函数传参
function SuperType(name){
this.name = name;
}
function SubType(name) {
SuperType.call(this, name);
}
let instance1 = new SubType("Nicholas");
console.log(instance1.name); // "Nicholas";
let instance2 = new SubType("Leo");
console.log(instance2.name); // "Leo";
“盗用构造函数”这种方式也有缺点,它必须在构造函数中定义方法,因此函数不能重用。另外,子类也不能访问父类原型上定义的方法,因此盗用构造函数基本不能单独使用。
组合继承综合了原型链和盗用构造函数,将两者的优点集中了起来。其思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
组合继承弥补了原型链和盗用构造函数的不足,是 JS 中使用最多的继承模式。
JS 可以通过 Object.create() 方法实现原型式继承。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个参数可选)。
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
Object.create() 的第二个参数与 Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
console.log(anotherPerson.name); // "Greg"
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
在组合继承的过程中,父类构造函数会被调用两次,因此会存在效率问题。
寄生式组合继承可以解决这个问题。寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。基本模式如下:
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
inheritPrototype 函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的 prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。完整示例如下:
// 定义父类
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
// 定义子类
function SubType(name, age) {
// 继承父类
SuperType.call(this, name);
this.age = age;
}
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此寄生式组合继承可以算是引用类型继承的最佳模式。
定义类可以通过两种主要方式:使用 class 关键字进行类声明和类表达式
// 类声明
class Person {}
// 类表达式
const Animal = class {};
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法。
// 空类定义,有效
class Foo {}
// 有构造函数的类,有效
class Bar {
constructor() {}
}
// 有获取函数的类,有效
class Baz {
get myBaz() {}
}
// 有静态方法的类,有效
class Qux {
static myQux() {}
}
constructor 关键字用于在类定义块内部创建类的构造函数,省略构造函数相当于将构造函数定义为空函数。
使用 new 操作符实例化 Person 的操作等于使用 new 调用其构造函数。
class Animal {}
class Person {
constructor() {
console.log('person ctor');
}
}
class Vegetable {
constructor() {
this.color = 'orange';
}
}
let a = new Animal();
let p = new Person();
let v = new Vegetable();
console.log(v.color);
类实例化时传入的参数会用作构造函数的参数,不传参数时可以省略类名后的括号
class Person {
constructor(name) {
console.log(arguments.length);
this.name = name || null;
}
}
let p1 = new Person; // 0
console.log(p1.name); // null
let p2 = new Person(); // 0
console.log(p2.name); // null
let p3 = new Person('Jake'); // 1
console.log(p3.name); // Jake
类构造函数执行后会返回 this 对象,不过也可以自定义返回的对象,但是这个自定义的对象不会通过 instanceof 操作符检测出与类关联
class Person {
constructor(override) {
this.foo = 'foo';
if (override) {
return {
bar: 'bar'
};
}
}
}
let p1 = new Person(),
p2 = new Person(true);
console.log(p1); // Person { foo: 'foo' }
console.log(p1 instanceof Person); // true
console.log(p2); // { bar: 'bar' }
console.log(p2 instanceof Person); // false
类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符,而普通构造函数如果不使用 new 调用,那么就会以全局的 this (通常是 window) 作为内部对象。
类本身具有与普通构造函数一样的行为,在类的上下文中,类本身在使用 new 调用时会被当成构造函数。重点在于,类中定义的 constructor 方法不会被当成构造函数,在对它使用 instanceof 操作符时会返回 false。但是,如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么 instanceof 操作符的返回值会返回true。
class Person {}
let p1 = new Person();
console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Person.constructor); // false
let p2 = new Person.constructor();
console.log(p2.constructor === Person); // false
console.log(p2 instanceof Person); // false
console.log(p2 instanceof Person.constructor); // true
每次通过 new 调用类标识符时,都会执行类构造函数。在这个函数内部,可以为新创建的实例添加“自有”属性。在构造函数执行完毕后,仍然可以给实例继续添加新成员。每个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享:
class Person {
constructor() {
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
this.nicknames = ['Jake', 'J-Dog']
}
}
let p1 = new Person(),
p2 = new Person();
p1.sayName(); // Jack
p2.sayName(); // Jack
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); // false
p1.name = p1.nicknames[0];
p2.name = p2.nicknames[1];
p1.sayName(); // Jake
p2.sayName(); // J-Dog
为了在实例间共享方法,类块中定义的方法会定义到原型上:
class Person {
constructor() {
// this 上的内容会存在于不同的实例上
this.locate = () => console.log('instance');
}
// 类块中定义的内容会出现在原型上
locate() {
console.log('prototype');
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
类定义也支持获取和设置访问器。语法与行为跟普通对象一样:
class Person {
set name(newName) {
this.name_ = newName;
}
get name() {
return this.name_;
}
}
可以在类上定义静态方法用于执行不特定于实例的操作,使用 static 关键字作为前缀:
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, {constructor: ... }
Person.locate(); // class, class Person {}
类定义语法支持在原型和类本身上定义生成器方法:
class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}
// 在类上定义生成器方法
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog
可以通过添加一个默认的迭代器,把类实例变成可迭代对象:
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
*[Symbol.iterator]() {
yield *this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog
ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有 [[Construct]] 和原型的对象。虽然类继承使用的是新语法,但本质上依旧使用的是原型链。
class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
function Person() {}
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true
派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。
class Vehicle {
constructor() {
this.hasEngine = true;
}
}
class Bus extends Vehicle {
constructor() {
// 不要在调用super()之前引用this,否则会抛出ReferenceError
super(); // 相当于super.constructor()
console.log(this instanceof Vehicle); // true
console.log(this); // Bus { hasEngine: true }
}
}
使用 super 时要注意几个问题
super 只能在派生类构造函数和静态方法中使用
不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法
调用 super() 会调用父类构造函数,并将返回的实例赋值给 this
super() 的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {
constructor(licensePlate) {
super(licensePlate);
}
}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
如果没有定义类构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类的参数
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
在类构造函数中,不能在调用 super() 之前引用 this
如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回一个对象
通过 new.target 可以创建一个抽象类,以阻止该类实例化:
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
}
}
// 派生类
class Bus extends Vehicle {}
new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
if (!this.foo) {
throw new Error('Inheriting class must define foo()');
}
console.log('success!');
}
}
// 派生类
class Bus extends Vehicle {
foo() {}
}
// 派生类
class Van extends Vehicle {}
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()
把不同类的行为集中到一个类是一种常见的 JavaScript 模式,一个实现思路是在 extends 关键字后面提供一个 JS 表达式:
class Vehicle {}
function getParentClass() {
console.log('evaluated expression');
return Vehicle;
}
class Bus extends getParentClass() {}
通过表达式就可以实现多个类的混合,例如 Person 类需要组合 A、B、C 类,则需要实现 B 继承 A,C 再继承 B,而 Person 继承 C 即可
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
foo() {
console.log('foo');
}
};
let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log('bar');
}
};
let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log('baz');
}
};
// 辅助函数
function mix(BaseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
一个众所周知的设计原则是“组合优于继承”,因此混合模式逐渐被抛弃,转向了组合模式,因此这里作为了解即可,将来能够看懂代码作者的设计用意。